/**
 * This file is part of the CAKIS project.
 * 
 * Copyright (c) 2009-2010 - Florian Arndt, Christian Kerl
 * 
 * @license GNU Lesser General Public License v3 <http://www.opensource.org/licenses/lgpl-3.0.html>
 * @since   01.03.2010
 * @version SVN: $Id: CodecheckProductLookupService.java 131 2010-05-13 22:12:31Z christian-kerl@web.de $
 */

package org.cakis.server.service.impl;

import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.cakis.server.Names;
import org.cakis.server.model.Product;
import org.cakis.server.model.ProductQuantity;
import org.cakis.server.model.StockUnit;
import org.cakis.server.service.ProductLookupService;
import org.cakis.server.service.util.HtmlContentHandlerExecutor;
import org.cakis.server.service.util.HtmlConstants;
import org.cakis.server.service.util.URLStreamFactory;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Implementation of the {@link ProductLookupService} interface for http://www.codecheck.info
 * providing the following information:
 * - product name
 * - product ean
 * - product standard quantity
 * - product stock unit name (might be the stock unit symbol)
 * 
 * @author Christian Kerl
 */
@Name(Names.CodecheckProductLookupService)
@Scope(ScopeType.APPLICATION)
@Startup
public class CodecheckProductLookupService extends ProductLookupServiceRegistryImpl.SelfRegisteringProductLookupService implements ProductLookupService {

    private static final String Name = "codecheck.info";
    private static final String BaseUrl = "http://www.codecheck.info";
    private static final String SearchUrl = "/Home/search/sdirect.pag?formids=If_2_0%2CsearchText2%2CIf_3_0%2CIf_7_0&submitmode=&submitname=&If_2_0=F&If_3_0=F&If_7_0=F&OK2=Suchen&searchText2=";

    private static final String Charset = "UTF-8";

    private final URLStreamFactory urlStreamFactory;

    /**
     * Default constructor initializing the instance with the {@link URLStreamFactory#DEFAULT} object.
     */
    public CodecheckProductLookupService() {
        this(URLStreamFactory.DEFAULT);
    }

    /**
     * Constructor initializing the instance with the given {@link URLStreamFactory} object.
     * 
     * @param factory The URLStreamFactory to use.
     */
    public CodecheckProductLookupService(URLStreamFactory factory) {
        urlStreamFactory = factory;
    }

    @Override
    public String getName() {
        return Name;
    }

    @Override
    public URL getUrl() {
        try {
            return new URL(BaseUrl);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Product getProductInformation(String ean) {
        if(ean == null || ean.length() < 1) {
            throw new IllegalArgumentException("ean");
        }

        HttpURLConnection.setFollowRedirects(true);

        ProductPageContentHandler handler = new ProductPageContentHandler();

        HtmlContentHandlerExecutor<ProductPageContentHandler> executor = new HtmlContentHandlerExecutor<ProductPageContentHandler>(urlStreamFactory, handler, Charset);
        executor.execute(getSearchUrl(ean));

        Product result = null;

        if(ean.equals(handler.getResult().getEanNumber())) {
            result = handler.getResult();
        }

        return result;
    }

    private URL getSearchUrl(String ean) {
        try {
            return new URL(getUrl(), SearchUrl + ean);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    private static class ProductPageContentHandler extends DefaultHandler implements HtmlConstants {
        private static enum State {
            // no SpanElement with class HeadingClass or SubHeadingClass detected
            Wait,
            // SpanElement with class HeadingClass detected, next text node is product name
            ExpectName,
            // SpanElement with class SubHeadingClass detected
            ExpectDetail,
            // SpanElement with class SubHeadingClass and text EanHeading detected, next text node is ean number
            ExpectEan,
            // SpanElement with class SubHeadingClass and text QuantityHeading detected, next text node is quantity
            ExpectQuantity,
        }

        // state transitions:
        // State.Wait           -> State.ExpectName | State.ExpectDetail
        // State.ExpectDetail   -> State.ExpectEan  | State.ExpectQuantity
        // State.ExpectName     -> State.Wait
        // State.ExpectEan      -> State.Wait
        // State.ExpectQuantity -> State.Wait

        // class for heading on codecheck.info (containing product name)
        private static final String HeadingClass    = "htit2";
        // class for sub heading on codecheck.info (containing additional product information)
        private static final String SubHeadingClass = "htit";

        // heading for ean number on codecheck.info
        private static final String EanHeading      = "EAN-Nummer";
        // heading for quantity on codecheck.info
        private static final String QuantityHeading = "Menge / Grösse";


        private final Product result;
        private State state;

        public Product getResult() {
            return result;
        }

        public ProductPageContentHandler() {
            ProductQuantity quantity = new ProductQuantity();
            quantity.setUnit(new StockUnit());

            result = new Product();
            result.setStandardQuantity(quantity);

            state = State.Wait;
        }

        @Override
        public void startElement(String uri, String name, String qName,
                Attributes attr) throws SAXException {
            String clazz = getAttrValue(attr, ClassAttribute);

            if(SpanElement.equals(name) && !clazz.isEmpty()) {
                if(HeadingClass.equals(clazz)) {
                    state = State.ExpectName;
                } else if(SubHeadingClass.equals(clazz)) {
                    state = State.ExpectDetail;
                }
            }
        }

        @Override
        public void characters(char[] ch, int start, int length)
        throws SAXException {
            String data = (new String(ch, start, length)).trim();

            switch (state) {
            case ExpectDetail:
                if(EanHeading.equals(data)) {
                    state = State.ExpectEan;
                } else if(QuantityHeading.equals(data)) {
                    state = State.ExpectQuantity;
                } else {
                    state = State.Wait;
                }
                break;
            case ExpectName:
                handleName(data);
                state = State.Wait;
                break;
            case ExpectEan:
                handleEan(data);
                state = State.Wait;
                break;
            case ExpectQuantity:
                handleQuantity(data);
                state = State.Wait;
                break;
            default:
                state = State.Wait;
                break;
            }
        }

        private void handleName(String data) {
            getResult().setName(data);
        }

        private void handleEan(String data) {
            getResult().setEanNumber(data);
        }

        private void handleQuantity(String data) {
            // examples:
            //  1,5 Liter
            //  500g
            //  20x3g=60g
            Matcher regex = Pattern.compile(
                    ".*?" +              // any character, expect total quantity at end of string
                    "(\\d+[,.]*\\d*)" +  // 1+ digit followed by decimal . or , and 0+ decimal digit = quantity
                    "\\s*" +             // 0+ whitespace
                    "(\\w+)"             // 1+ word characters = stock unit
            ).matcher(data);

            if(!regex.matches()) {
                return;
            }

            try {
                Float stdQuantity = Float.parseFloat(regex.group(1).replace(',', '.'));

                getResult().getStandardQuantity().setQuantity(stdQuantity);
            } catch(NumberFormatException e) {
            }

            getResult().getStandardQuantity().getUnit().setName(regex.group(2).trim());
        }

        private String getAttrValue(Attributes attr, String name) {
            String value = attr.getValue(name);

            return value != null ? value : "";
        }
    }
}
